package com.ybg.utils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 * 表主键管理类。通过调用nextVal(String tableName)获取表的下一个序列值。
 * 
 * @author  ningjh
 * @since   2016-07-20
 */
@Repository
public final class IdUtil {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    private final static Map<String, Sequence> SEQUENCE_CACHE = new HashMap<String, Sequence>();
    private final static String                TABLE_SEQUENCE = "table_sequence";
    private final static long                  SEQUENCE_RANGE = 20L;
    
    /**
     * 初始化'table_sequence'表。由Spring装载该类时执行。
     * 若使用MySQL数据库，该方法会自动生成数据；若是其它数据库，则需要手动向表中插入数据。
     */
    @PostConstruct
    private void init() {
        try {
            initSequence();
        } catch(Exception e) {
        	LogUtil.error("请检查是否存在表'{}', 或者该表是否已经初始化", TABLE_SEQUENCE, e);
            System.exit(-1);//主键管理表初始化失败，则应该停止JVM
        }
    }
    
    /**
     * 初始化主键管理表。加载数据到内存。
     */
    private void initSequence() {
        List<String> tableNames = initTableSequence();
        
        if(tableNames != null) {
        	for(String tableName : tableNames) {
        		if(TABLE_SEQUENCE.equals(tableName)) {
        			continue;
        		} else {
        			SEQUENCE_CACHE.put(tableName, new Sequence(tableName, 0, 0));
        		}
        	}
        }
    }
    
    /**
     * 如果是MySQL数据库，则自动插入数据。
     */
    private List<String> initTableSequence() {
    	// 数据库中所有表的名称
    	List<String> tableNames = null;
    	
    	// 获取MySQL数据库中的所有表
    	try {
    		tableNames = jdbcTemplate.query("show tables", new RowMapper<String>() {
				@Override
				public String mapRow(ResultSet rs, int rowNum) throws SQLException {
					return rs.getString(1).toLowerCase();
				}
    		});
    	} catch(Exception e) {
    		LogUtil.error("IdUtil只支持MySQL自动初始化，其它数据库请手段初始化表 {}", TABLE_SEQUENCE);
    		throw e;
    	}
    	
    	// 初始化表ID
    	if(tableNames != null && !tableNames.isEmpty()) {
    		for(String tableName : tableNames) {
    			if(TABLE_SEQUENCE.equals(tableName)) continue;
    			
    			String sql = "select count(*) from " + TABLE_SEQUENCE + " where table_name = ?";
    			int count = jdbcTemplate.queryForObject(sql, Integer.class, tableName);
    			
    			if(count == 0) {
    				try {
    					sql = "insert into " + TABLE_SEQUENCE + "(table_name, sequence) values(?, ?)";
    					jdbcTemplate.update(sql, tableName, 0);
    				} catch(Exception e) {
    					LogUtil.error("记录 {} 已存在", tableName, e);
    				}
    			}
    		}
    	}
    	
    	return tableNames;
    }
    
    /**
     * 根据表名，获取该表的下一个ID序号。
     * 该方法是线程安全的。
     *
     * @param tableName
     * @return
     */
    public long nextVal(String tableName) {
        Sequence sequence = SEQUENCE_CACHE.get(tableName);
        
        synchronized (sequence) {
            return nextVal(sequence);
        }
    }
    
    /**
     * 申请一段未使用的ID段放到内存中，使用完后再重新申请。
     *
     * @param sequence
     * @return
     */
    private long nextVal(Sequence sequence) {
    	if(sequence.currentSequence >= sequence.lastSequence) {
    		getAndUpdate(sequence);
    	}
        
        return ++sequence.currentSequence;
    }
    
    @Transactional
    private void getAndUpdate(Sequence sequence) {
		try {
	    	// 让数据库在该记录上加上行级锁，避免多线程同时修改
	    	String sql = "update " + TABLE_SEQUENCE + " set sequence = sequence + " + SEQUENCE_RANGE + " where table_name = ?";
	    	jdbcTemplate.update(sql, sequence.tableName);
	    	
	    	// 获取一段未使用的ID
	    	sql = "select sequence from " + TABLE_SEQUENCE + " where table_name = ?";
	        sequence.lastSequence = jdbcTemplate.queryForObject(sql, Long.class, sequence.tableName);
	        sequence.currentSequence = sequence.lastSequence - SEQUENCE_RANGE;
		} catch(Exception e) {
			LogUtil.error("获取表'{}'的ID出错", sequence.tableName);
			throw new YbgException(e);
		}
    }
    
    private class Sequence {
        String tableName;
        long   currentSequence;
        long   lastSequence;
        
        public Sequence(String tableName, long currentSequence, long lastSequence) {
            this.tableName       = tableName;
            this.currentSequence = currentSequence;
            this.lastSequence    = lastSequence;
        }
    }
}
